msg_tool\scripts\hexen_haus\archive/
arcc.rs

1//! HexenHaus ARCC archive (.arc)
2use crate::ext::io::*;
3use crate::scripts::base::*;
4use crate::types::*;
5use crate::utils::encoding::decode_to_string;
6use anyhow::Result;
7use std::io::{Read, Seek, SeekFrom};
8use std::sync::{Arc, Mutex};
9
10#[derive(Debug)]
11/// HexenHaus ARCC archive builder
12pub struct HexenHausArccArchiveBuilder;
13
14impl HexenHausArccArchiveBuilder {
15    /// Creates a new `HexenHausArccArchiveBuilder`
16    pub const fn new() -> Self {
17        HexenHausArccArchiveBuilder
18    }
19}
20
21impl ScriptBuilder for HexenHausArccArchiveBuilder {
22    fn default_encoding(&self) -> Encoding {
23        Encoding::Cp932
24    }
25
26    fn default_archive_encoding(&self) -> Option<Encoding> {
27        Some(Encoding::Cp932)
28    }
29
30    fn build_script(
31        &self,
32        buf: Vec<u8>,
33        _filename: &str,
34        _encoding: Encoding,
35        archive_encoding: Encoding,
36        config: &ExtraConfig,
37        _archive: Option<&Box<dyn Script>>,
38    ) -> Result<Box<dyn Script>> {
39        Ok(Box::new(HexenHausArccArchive::new(
40            MemReader::new(buf),
41            archive_encoding,
42            config,
43        )?))
44    }
45
46    fn build_script_from_file(
47        &self,
48        filename: &str,
49        _encoding: Encoding,
50        archive_encoding: Encoding,
51        config: &ExtraConfig,
52        _archive: Option<&Box<dyn Script>>,
53    ) -> Result<Box<dyn Script>> {
54        if filename == "-" {
55            let data = crate::utils::files::read_file(filename)?;
56            return Ok(Box::new(HexenHausArccArchive::new(
57                MemReader::new(data),
58                archive_encoding,
59                config,
60            )?));
61        }
62        let file = std::fs::File::open(filename)?;
63        let reader = std::io::BufReader::new(file);
64        Ok(Box::new(HexenHausArccArchive::new(
65            reader,
66            archive_encoding,
67            config,
68        )?))
69    }
70
71    fn build_script_from_reader(
72        &self,
73        reader: Box<dyn ReadSeek>,
74        _filename: &str,
75        _encoding: Encoding,
76        archive_encoding: Encoding,
77        config: &ExtraConfig,
78        _archive: Option<&Box<dyn Script>>,
79    ) -> Result<Box<dyn Script>> {
80        Ok(Box::new(HexenHausArccArchive::new(
81            reader,
82            archive_encoding,
83            config,
84        )?))
85    }
86
87    fn extensions(&self) -> &'static [&'static str] {
88        &["arc"]
89    }
90
91    fn script_type(&self) -> &'static ScriptType {
92        &ScriptType::HexenHausArcc
93    }
94
95    fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
96        if buf_len >= 4 && buf.starts_with(b"ARCC") {
97            Some(10)
98        } else {
99            None
100        }
101    }
102
103    fn is_archive(&self) -> bool {
104        true
105    }
106}
107
108#[derive(Debug, Clone)]
109struct HexenHausArccEntry {
110    name: String,
111    offset: u64,
112    size: u32,
113}
114
115#[derive(Debug)]
116/// HexenHaus ARCC archive
117pub struct HexenHausArccArchive<T: Read + Seek + std::fmt::Debug> {
118    reader: Arc<Mutex<T>>,
119    entries: Vec<HexenHausArccEntry>,
120}
121
122impl<T: Read + Seek + std::fmt::Debug> HexenHausArccArchive<T> {
123    /// Creates a new `HexenHausArccArchive`
124    pub fn new(mut reader: T, archive_encoding: Encoding, _config: &ExtraConfig) -> Result<Self> {
125        reader.seek(SeekFrom::Start(0))?;
126        let mut signature = [0u8; 4];
127        reader.read_exact(&mut signature)?;
128        if signature != *b"ARCC" {
129            return Err(anyhow::anyhow!("Invalid HexenHaus ARCC signature"));
130        }
131        reader.seek(SeekFrom::Start(0))?;
132        let reader = Arc::new(Mutex::new(reader));
133
134        let file_count = reader.cpeek_u32_at(0x14)?;
135        let entry_count = file_count as usize;
136
137        let mut index_offset = 0x2a_u64;
138        let mut tag = [0u8; 4];
139        reader.cpeek_exact_at(index_offset, &mut tag)?;
140        if &tag != b"NAME" {
141            return Err(anyhow::anyhow!("Missing NAME section in ARCC archive"));
142        }
143        let addr_offset = reader.cpeek_u64_at(index_offset + 4)?;
144        index_offset += 0x0e;
145
146        reader.cpeek_exact_at(index_offset, &mut tag)?;
147        if &tag != b"NIDX" {
148            return Err(anyhow::anyhow!("Missing NIDX section in ARCC archive"));
149        }
150        index_offset += 4;
151        for _ in 0..entry_count {
152            let _ = reader.cpeek_u32_at(index_offset + 2)?;
153            index_offset += 8;
154        }
155
156        reader.cpeek_exact_at(index_offset, &mut tag)?;
157        if &tag != b"EIDX" {
158            return Err(anyhow::anyhow!("Missing EIDX section in ARCC archive"));
159        }
160        index_offset += 4 + 8 * file_count as u64;
161
162        reader.cpeek_exact_at(index_offset, &mut tag)?;
163        if &tag != b"CINF" {
164            return Err(anyhow::anyhow!("Missing CINF section in ARCC archive"));
165        }
166        index_offset += 4;
167
168        let mut entries = Vec::with_capacity(entry_count);
169        for _ in 0..entry_count {
170            index_offset += 6;
171            let name_len = reader.cpeek_u16_at(index_offset)? as usize;
172            let mut name_buf = vec![0u8; name_len];
173            if name_len > 0 {
174                reader.cpeek_exact_at(index_offset + 4, &mut name_buf)?;
175                decrypt_name(&mut name_buf);
176            }
177            index_offset += 6 + name_len as u64;
178            let name = decode_to_string(archive_encoding, &name_buf, true)?;
179            entries.push(HexenHausArccEntry {
180                name,
181                offset: 0,
182                size: 0,
183            });
184        }
185
186        let mut addr_offset = addr_offset;
187        reader.cpeek_exact_at(addr_offset, &mut tag)?;
188        if &tag != b"ADDR" {
189            return Err(anyhow::anyhow!("Missing ADDR section in ARCC archive"));
190        }
191        addr_offset += 4;
192        for entry in &mut entries {
193            entry.offset = reader.cpeek_u64_at(addr_offset + 2)?;
194            addr_offset += 12;
195        }
196
197        for entry in &mut entries {
198            if reader.cpeek_and_equal_at(entry.offset, b"FILE").is_err() {
199                continue;
200            }
201            entry.size = reader.cpeek_u32_at(entry.offset + 0x18)?;
202            entry.offset += 0x22;
203        }
204
205        entries.retain(|entry| entry.size > 0);
206        if entries.is_empty() {
207            return Err(anyhow::anyhow!("ARCC archive contains no files"));
208        }
209
210        Ok(HexenHausArccArchive { reader, entries })
211    }
212}
213
214impl<T: Read + Seek + std::fmt::Debug + std::any::Any> Script for HexenHausArccArchive<T> {
215    fn default_output_script_type(&self) -> OutputScriptType {
216        OutputScriptType::Json
217    }
218
219    fn default_format_type(&self) -> FormatOptions {
220        FormatOptions::None
221    }
222
223    fn is_archive(&self) -> bool {
224        true
225    }
226
227    fn iter_archive_filename<'a>(
228        &'a self,
229    ) -> Result<Box<dyn Iterator<Item = Result<String>> + 'a>> {
230        Ok(Box::new(
231            self.entries.iter().map(|entry| Ok(entry.name.clone())),
232        ))
233    }
234
235    fn iter_archive_offset<'a>(&'a self) -> Result<Box<dyn Iterator<Item = Result<u64>> + 'a>> {
236        Ok(Box::new(self.entries.iter().map(|entry| Ok(entry.offset))))
237    }
238
239    fn open_file<'a>(&'a self, index: usize) -> Result<Box<dyn ArchiveContent + 'a>> {
240        if index >= self.entries.len() {
241            return Err(anyhow::anyhow!(
242                "Index out of bounds: {} (total files: {})",
243                index,
244                self.entries.len()
245            ));
246        }
247        let entry = &self.entries[index];
248        let header = self
249            .reader
250            .cpeek_at_vec(entry.offset, (entry.size as usize).min(16))?;
251        Ok(Box::new(Entry {
252            reader: self.reader.clone(),
253            header: entry.clone(),
254            pos: 0,
255            typ: super::detect_script_type(&entry.name, &header),
256        }))
257    }
258}
259
260struct Entry<T: Read + Seek> {
261    header: HexenHausArccEntry,
262    reader: Arc<Mutex<T>>,
263    pos: u64,
264    typ: Option<ScriptType>,
265}
266
267impl<T: Read + Seek> ArchiveContent for Entry<T> {
268    fn name(&self) -> &str {
269        &self.header.name
270    }
271
272    fn script_type(&self) -> Option<&ScriptType> {
273        self.typ.as_ref()
274    }
275}
276
277impl<T: Read + Seek> Read for Entry<T> {
278    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
279        let mut reader = self.reader.lock().map_err(|e| {
280            std::io::Error::new(
281                std::io::ErrorKind::Other,
282                format!("Failed to lock mutex: {}", e),
283            )
284        })?;
285        reader.seek(SeekFrom::Start(self.header.offset + self.pos))?;
286        let bytes_read = buf.len().min(self.header.size as usize - self.pos as usize);
287        if bytes_read == 0 {
288            return Ok(0);
289        }
290        let bytes_read = reader.read(&mut buf[..bytes_read])?;
291        self.pos += bytes_read as u64;
292        Ok(bytes_read)
293    }
294}
295
296impl<T: Read + Seek> Seek for Entry<T> {
297    fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
298        let new_pos = match pos {
299            SeekFrom::Start(offset) => offset as u64,
300            SeekFrom::End(offset) => {
301                if offset < 0 {
302                    if (-offset) as u64 > self.header.size as u64 {
303                        return Err(std::io::Error::new(
304                            std::io::ErrorKind::InvalidInput,
305                            "Seek from end exceeds file length",
306                        ));
307                    }
308                    self.header.size as u64 - (-offset) as u64
309                } else {
310                    self.header.size as u64 + offset as u64
311                }
312            }
313            SeekFrom::Current(offset) => {
314                if offset < 0 {
315                    if (-offset) as u64 > self.pos {
316                        return Err(std::io::Error::new(
317                            std::io::ErrorKind::InvalidInput,
318                            "Seek from current exceeds current position",
319                        ));
320                    }
321                    self.pos.saturating_sub((-offset) as u64)
322                } else {
323                    self.pos + offset as u64
324                }
325            }
326        };
327        self.pos = new_pos;
328        Ok(self.pos)
329    }
330
331    fn stream_position(&mut self) -> std::io::Result<u64> {
332        Ok(self.pos)
333    }
334}
335
336fn decrypt_name(buf: &mut [u8]) {
337    for byte in buf.iter_mut() {
338        *byte ^= 0x69;
339    }
340}